#include "Subsystems/BlockBehaviors/SubsystemFireBlockBehavior.h"
#include "Subsystems/SubsystemTimeOfDay.h"
#include "Subsystems/SubsystemTerrain.h"
#include "Subsystems/SubsystemGameInfo.h"

#include "Managers/BlocksManager.h"
#include "Network/PackageManager.h"
#include "Network/Packages/ComponentOnFirePackage.h"

#include "Blocks/FireBlock.h"
#include "Blocks/SnowballBlock.h"
#include "Blocks/SnowBlock.h"
#include "Blocks/AirBlock.h"

#include "GameSingleton.h"
#include "GameRandom.hpp"
#include "Log.h"


namespace PocketSurvival
{

    std::vector<Point3> SubsystemFireBlockBehavior::ExpansionVec = 
    {
        Point3( 1,  1,  1), Point3( 1,  1,  0), Point3( 1,  1, -1), 
        Point3( 1,  0,  1), Point3( 1,  0,  0), Point3( 1,  0, -1), 
        Point3( 1, -1,  1), Point3( 1, -1,  0), Point3( 1, -1, -1),

        Point3( 0,  1,  1), Point3( 0,  1,  0), Point3( 0,  1, -1), 
        Point3( 0,  0,  1)                    , Point3( 0,  0, -1), 
        Point3( 0, -1,  1), Point3( 0, -1,  0), Point3( 0, -1, -1),

        Point3(-1,  1,  1), Point3(-1,  1,  0), Point3(-1,  1, -1), 
        Point3(-1,  0,  1), Point3(-1,  0,  0), Point3(-1,  0, -1), 
        Point3(-1, -1,  1), Point3(-1, -1,  0), Point3(-1, -1, -1)
    };

    SubsystemFireBlockBehavior::SubsystemFireBlockBehavior()
    {
        m_handleBlocks.push_back(FireBlock::Index());
    }

    bool SubsystemFireBlockBehavior::isCellOnFire(const Point3& point)
    {
        for (int32_t i = 0; i < 4; i++)
        {
            Point3 facePoint = CellFace::FaceToPoint3(i);
            int32_t cellValue = m_terrain->getCellValue(point + facePoint);
            if (Terrain::ExtractContents(cellValue) == FireBlock::Index())
            {
                int32_t num = Terrain::ExtractData(cellValue);
                int32_t num2 = CellFace::OppositeFace(i);
                if ((num & (1 << num2)) != 0)
                {
                    return true;
                }
            }
        }
        int32_t cellValue2 = m_terrain->getCellValue(point.posX, point.posY + 1, point.posZ);
        if (Terrain::ExtractContents(cellValue2) == FireBlock::Index() && Terrain::ExtractData(cellValue2) == 0)
        {
            return true;
        }
        return false;
    }

    bool SubsystemFireBlockBehavior::setCellOnFire(const Point3& point, float fireExpandability, ComponentMiner *miner)
    {
        int32_t cellValue = m_terrain->getCellValue(point);
        int32_t num = Terrain::ExtractContents(cellValue);
        if (GameSingleton::blocksManager->blocks[num]->FireDuration == 0.0f)
        {
            return false;
        }
        bool result = false;
        for (int32_t i = 0; i < 5; i++)
        {
            Point3 facePoint = CellFace::FaceToPoint3(i);
            Point3 tarPoint = facePoint + point;

            int32_t cellValue2 = m_terrain->getCellValue(tarPoint);
            int32_t tarCellContents = Terrain::ExtractContents(cellValue2);
            
            if (tarCellContents == AirBlock::Index() || tarCellContents == FireBlock::Index() || tarCellContents == SnowBlock::Index())
            {
                int32_t num3 = (tarCellContents == FireBlock::Index()) ? Terrain::ExtractData(cellValue2) : AirBlock::Index();
                int32_t num4 = CellFace::OppositeFace(i);
                num3 |= ((1 << num4) & 0xF);
                cellValue = Terrain::ReplaceData(Terrain::ReplaceContents(0, FireBlock::Index()), num3);
                addFire(tarPoint, fireExpandability);
                m_terrain->changeCell(tarPoint, cellValue);
                result = true;
            }
        }
        return result;
    }

    void SubsystemFireBlockBehavior::addFire(const Point3& point, float expandability)
    {
        GameSingleton::packageManager->addSendPackageToQueue(std::make_shared<ComponentOnFirePackage>(point, expandability));
        addFireNet(point, expandability);
    }

    void SubsystemFireBlockBehavior::addFireNet(const Point3& point, float expandability)
    {
        std::lock_guard<SpinLock> lk(m_fireDataMapLock);
        if (m_fireDataMap.find(point) == m_fireDataMap.end())
        {
            FireData fireData;
            fireData.point = point;
            fireData.fireExpandability = expandability;
            initializeFireDataTimes(fireData);
            m_fireDataMap[point] = fireData;
        }
    }

    void SubsystemFireBlockBehavior::removeFire(const Point3& point)
    {
        GameSingleton::packageManager->addSendPackageToQueue(std::make_shared<ComponentOnFirePackage>(point));
        removeFireNet(point);
    }

    void SubsystemFireBlockBehavior::removeFireNet(const Point3& point)
    {
        std::lock_guard<SpinLock> lk(m_fireDataMapLock);
        auto it = m_fireDataMap.find(point);
        if (it != m_fireDataMap.end())
        {
            m_fireDataMap.erase(it);
        }
    }

    void SubsystemFireBlockBehavior::initializeFireDataTimes(SubsystemFireBlockBehavior::FireData &fireData)
    {
        initializeFireDataTime(fireData, 0);
        initializeFireDataTime(fireData, 1);
        initializeFireDataTime(fireData, 2);
        initializeFireDataTime(fireData, 3);
        initializeFireDataTime(fireData, 5);
    }

    void SubsystemFireBlockBehavior::initializeFireDataTime(SubsystemFireBlockBehavior::FireData &fireData, int32_t face)
    {
        Point3 point = CellFace::FaceToPoint3(face);
        int32_t x = fireData.point.posX + point.posX;
        int32_t y = fireData.point.posY + point.posY;
        int32_t z = fireData.point.posZ + point.posZ;
        int32_t cellContents = m_terrain->getCellContents(x, y, z);
        Block *block = GameSingleton::blocksManager->blocks[cellContents];
        switch (face)
        {
        case 4:
            break;
        case 0:
            fireData.time0 = block->FireDuration * GameSingleton::gameRandom->getFloat(0.75f, 1.25f);
            break;
        case 1:
            fireData.time1 = block->FireDuration * GameSingleton::gameRandom->getFloat(0.75f, 1.25f);
            break;
        case 2:
            fireData.time2 = block->FireDuration * GameSingleton::gameRandom->getFloat(0.75f, 1.25f);
            break;
        case 3:
            fireData.time3 = block->FireDuration * GameSingleton::gameRandom->getFloat(0.75f, 1.25f);
            break;
        case 5:
            fireData.time5 = block->FireDuration * GameSingleton::gameRandom->getFloat(0.75f, 1.25f);
            break;
        }
    }

    void SubsystemFireBlockBehavior::queueBurnAway(const Point3& point, float expandability)
    {
        m_toBurnAway[point] = expandability;
    }


    void SubsystemFireBlockBehavior::onPoll(int32_t value, int32_t x, int32_t y, int32_t z)
    {
        bool isInMap = false;
        Point3 point(x,y,z);

        m_fireDataMapLock.lock();
        if(m_fireDataMap.find(point) != m_fireDataMap.end())
        {
            isInMap = true;
        }
        m_fireDataMapLock.unlock();

        if(isInMap == false)
        {
            addFireNet(point, 1.0f);
        }
    }

    void SubsystemFireBlockBehavior::onNeighborBlockChanged(int32_t x, int32_t y, int32_t z, int32_t neighborX, int32_t neighborY, int32_t neighborZ)
    {
        int32_t cellValue = m_terrain->getCellValue(x, y, z);
        int32_t num = Terrain::ExtractData(cellValue);
        if ((num & 1) != 0 && GameSingleton::blocksManager->blocks[m_terrain->getCellContents(x, y, z + 1)]->FireDuration == 0.0f)
        {
            num &= -2;
        }
        if ((num & 2) != 0 && GameSingleton::blocksManager->blocks[m_terrain->getCellContents(x + 1, y, z)]->FireDuration == 0.0f)
        {
            num &= -3;
        }
        if ((num & 4) != 0 && GameSingleton::blocksManager->blocks[m_terrain->getCellContents(x, y, z - 1)]->FireDuration == 0.0f)
        {
            num &= -5;
        }
        if ((num & 8) != 0 && GameSingleton::blocksManager->blocks[m_terrain->getCellContents(x - 1, y, z)]->FireDuration == 0.0f)
        {
            num &= -9;
        }

        m_fireDataMapLock.lock();
        auto fireIt = m_fireDataMap.find(Point3(x, y, z));
        if (fireIt != m_fireDataMap.end())
        {
            if ((num & 1) != 0 && neighborX == x && neighborY == y && neighborZ == z + 1)
            {
                initializeFireDataTime(fireIt->second, 0);
            }
            if ((num & 2) != 0 && neighborX == x + 1 && neighborY == y && neighborZ == z)
            {
                initializeFireDataTime(fireIt->second, 1);
            }
            if ((num & 4) != 0 && neighborX == x && neighborY == y && neighborZ == z - 1)
            {
                initializeFireDataTime(fireIt->second, 2);
            }
            if ((num & 8) != 0 && neighborX == x - 1 && neighborY == y && neighborZ == z)
            {
                initializeFireDataTime(fireIt->second, 3);
            }
            if (num == 0 && neighborX == x && neighborY == y - 1 && neighborZ == z)
            {
                initializeFireDataTime(fireIt->second, 5);
            }
        }
        m_fireDataMapLock.unlock();

        int32_t contents = FireBlock::Index();
        if (num == 0 && GameSingleton::blocksManager->blocks[m_terrain->getCellContents(x, y - 1, z)]->FireDuration == 0.0f)
        {
            contents = 0;
        }
        int32_t value2 = Terrain::ReplaceData(Terrain::ReplaceContents(0, contents), num);
        m_terrain->changeOldCell(x, y, z, cellValue, value2);
    }

    void SubsystemFireBlockBehavior::onBlockAdded(int32_t value, int32_t oldValue, int32_t x, int32_t y, int32_t z)
    {
        addFire(Point3(x, y, z), 1.0f);
    }

    void SubsystemFireBlockBehavior::onBlockRemoved(int32_t value, int32_t newValue, int32_t x, int32_t y, int32_t z)
    {
        removeFire(Point3(x, y, z));
    }

    void SubsystemFireBlockBehavior::onBlockGenerated(int32_t value, int32_t x, int32_t y, int32_t z, bool isLoaded)
    {
        addFire(Point3(x, y, z), 1.0f);
    }

    void SubsystemFireBlockBehavior::onChunkDiscarding(TerrainChunk *chunk)
    {
        std::map<Point3, FireData> newFireDataMap;

        std::lock_guard<SpinLock> lk(m_fireDataMapLock);
        for (auto &pair : m_fireDataMap)
        {
            if (false == (pair.first.posX >= chunk->origin.posX && pair.first.posX < chunk->origin.posX + 16 &&
                          pair.first.posZ >= chunk->origin.posY && pair.first.posZ < chunk->origin.posY + 16))
            {
                newFireDataMap.insert(pair);
            }
        }
        m_fireDataMap.swap(newFireDataMap);
    }

    void SubsystemFireBlockBehavior::update(float dt)
    {
        if(m_fireHandleVec.size() > 0)
        {
            int32_t num = 0;
            while(num < 50 && m_fireHandleVecIndex < m_fireHandleVec.size())
            {
                FireData &fireData = m_fireHandleVec[m_fireHandleVecIndex];
                int32_t cellValue = m_terrain->getCellValue(fireData.point);
                int32_t cellData = Terrain::ExtractData(cellValue);

                if((cellData & 1) > 0 && fireData.time0 > 0.0f)
                {
                    fireData.time0 -= 5.0f;
                    if(fireData.time0 <= 0.0f)
                    {
                        Point3 point = fireData.point;
                        point.posZ += 1;
                        queueBurnAway(point, fireData.fireExpandability * m_fireDiffusionReduction);
                    }
                    for(const Point3& expandPoint : ExpansionVec)
                    {
                        if(GameSingleton::gameRandom->getBool(fireData.fireExpandability * m_fireDiffusionReduction))
                        {
                            Point3 point = expandPoint + fireData.point;
                            point.posZ += 1;
                            m_toExpand[point] = MathUtils::Min<float>(0.5f, fireData.fireExpandability * m_fireDiffusionReduction);
                        }
                    }
                }
                if((cellData & 2) > 0 && fireData.time1 > 0.0f)
                {
                    fireData.time1 -= 5.0f;
                    if(fireData.time1 <= 0.0f)
                    {
                        Point3 point = fireData.point;
                        point.posX += 1;
                        queueBurnAway(point, fireData.fireExpandability * m_fireDiffusionReduction);
                    }
                    for(const Point3& expandPoint : ExpansionVec)
                    {
                        if(GameSingleton::gameRandom->getBool(fireData.fireExpandability * m_fireDiffusionReduction))
                        {
                            Point3 point = expandPoint + fireData.point;
                            point.posX += 1;
                            m_toExpand[point] = MathUtils::Min<float>(0.5f, fireData.fireExpandability * m_fireDiffusionReduction);
                        }
                    }
                }
                if((cellData & 4) > 0 && fireData.time2 > 0.0f)
                {
                    fireData.time2 -= 5.0f;
                    if(fireData.time2 <= 0.0f)
                    {
                        Point3 point = fireData.point;
                        point.posZ -= 1;
                        queueBurnAway(point, fireData.fireExpandability * m_fireDiffusionReduction);
                    }
                    for(const Point3& expandPoint : ExpansionVec)
                    {
                        if(GameSingleton::gameRandom->getBool(fireData.fireExpandability * m_fireDiffusionReduction))
                        {
                            Point3 point = expandPoint + fireData.point;
                            point.posZ -= 1;
                            m_toExpand[point] = MathUtils::Min<float>(0.5f, fireData.fireExpandability * m_fireDiffusionReduction);
                        }
                    }
                }
                if((cellData & 8) > 0 && fireData.time3 > 0.0f)
                {
                    fireData.time3 -= 5.0f;
                    if(fireData.time3 <= 0.0f)
                    {
                        Point3 point = fireData.point;
                        point.posX -= 1;
                        queueBurnAway(point, fireData.fireExpandability * m_fireDiffusionReduction);
                    }
                    for(const Point3& expandPoint : ExpansionVec)
                    {
                        if(GameSingleton::gameRandom->getBool(fireData.fireExpandability * m_fireDiffusionReduction))
                        {
                            Point3 point = expandPoint + fireData.point;
                            point.posX -= 1;
                            m_toExpand[point] = MathUtils::Min<float>(0.5f, fireData.fireExpandability * m_fireDiffusionReduction);
                        }
                    }
                }
                if(cellData == 0 && fireData.time5 > 0)
                {
                    fireData.time5 -= 5.0f;
                    if(fireData.time5 <= 0.0f)
                    {
                        Point3 point = fireData.point;
                        point.posY -= 1;
                        queueBurnAway(point, fireData.fireExpandability * m_fireDiffusionReduction);
                    }
                    for(const Point3& expandPoint : ExpansionVec)
                    {
                        if(GameSingleton::gameRandom->getBool(fireData.fireExpandability * m_fireDiffusionReduction))
                        {
                            Point3 point = expandPoint + fireData.point;
                            m_toExpand[point] = MathUtils::Min<float>(0.5f, fireData.fireExpandability * m_fireDiffusionReduction);
                        }
                    }
                }
                ++num;
                ++m_fireHandleVecIndex;
            }
        }

        if (m_timeOfDay->periodicGameTimeEvent(5.0f, 0.0f) == true)
        {
            for(auto &pair : m_toBurnAway)
            {
                m_terrain->changeCell(pair.first, AirBlock::Index());
            }
            for(const FireData& fireData : m_fireHandleVec)
            {
                m_fireDataMapLock.lock();
                auto it = m_fireDataMap.find(fireData.point);
                if(it != m_fireDataMap.end())
                {
                    it->second.time0 = fireData.time0;
                    it->second.time1 = fireData.time1;
                    it->second.time2 = fireData.time2;
                    it->second.time3 = fireData.time3;
                    it->second.time5 = fireData.time5;
                }
                m_fireDataMapLock.unlock();
            }
            for(auto &pair : m_toExpand)
            {
                setCellOnFire(pair.first, pair.second);
            }
            m_fireHandleVec.clear();
            m_toBurnAway.clear();
            m_toExpand.clear();
            
            m_fireDataMapLock.lock();
            for(const auto& pair : m_fireDataMap)
            {
                m_fireHandleVec.push_back(pair.second);
            }
            m_fireDataMapLock.unlock();
            m_fireHandleVecIndex = 0;
        }
    }

    void SubsystemFireBlockBehavior::load(const nlohmann::json &json)
    {
        m_timeOfDay = GameSingleton::singletonPtr->getSingleton<SubsystemTimeOfDay>();
        m_terrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
        m_gameInfo = GameSingleton::singletonPtr->getSingleton<SubsystemGameInfo>();
    }

    static const std::string subsystemName = "FireBlockBehavior";
    const std::string &SubsystemFireBlockBehavior::getName() const
    {
        return subsystemName;
    }
    SubUpdateType SubsystemFireBlockBehavior::getUpdateType()
    {
        return SubUpdateType::MultiThreadUpdate;
    }
}
